import slick.codegen.SourceCodeGenerator
import slick.model.Model

import scala.concurrent.duration.Duration
import scala.concurrent.{Await, ExecutionContext}

/**
 * Created by corbin on 15/9/5.
 *
 */


object SlickCodeGenerator extends App with GeneralCode {

  import Config._

  final val floder = "src/main/scala"

  val path = scala.reflect.io.Directory(s"$floder/${pkg.split('.').init.mkString("/")}")

  scala.util.Try {
    path.deepFiles.filter(_.name.endsWith(".scala")) foreach (_.delete())
    path.dirs foreach (_.delete())
    path delete()
  }


  val dbFactory = driver.api.Database
  val db = dbFactory.forURL(url, driver = jdbcDriver, user = username, password = password, keepAliveConnection = true)
  try {
    val m = Await.result(db.run(driver.createModel(None, ignoreInvalidDefaults = false)(ExecutionContext.global).withPinnedSession), Duration.Inf)
    val codegen = new SlickCodeGenerator(m)
    codegen.writeStringToFile(pagingCode(pkg), floder, pkg, "Paging.scala")
    val jsonsCode =
      s"""
package $pkg

import play.api.libs.json.Json
import Models._
/**
*
*Created by corbin
*/
object Common4Json{
${
        codegen.tables.map(_.EntityType.rawName).map { Clazz =>
          s"\timplicit val ${Clazz}Format = Json.format[$Clazz]\n"
        }.mkString("")
      }
}
   """.trim

    val optionParentPackage = Option(pkg.split('.').init.mkString(".").trim)
    val parentPackage = optionParentPackage.getOrElse("").trim
    // dbConfig codegen
    val supportPackage = if (parentPackage.isEmpty) "support" else s"$parentPackage.support"
    codegen.writeStringToFile(MysqlSupport(supportPackage), floder, supportPackage, "MysqlSupport.scala")
    codegen.writeStringToFile(MysqlConfig(supportPackage), floder, supportPackage, "MysqlConfig.scala")

    //dao codegen
    val daoPackage = if (parentPackage.isEmpty) "dao" else s"$parentPackage.dao"
    codegen.writeStringToFile(BaseDao(optionParentPackage), floder, daoPackage, "BaseDao.scala")

    codegen.writeStringToFile(jsonsCode, floder, pkg, "Common4Json.scala")
    codegen.writeToFile(slickDriver, floder, pkg, "Models", "Models.scala")

  } finally db.close()

}


class SlickCodeGenerator(model: Model) extends SourceCodeGenerator(model) {
  gen =>

  override def tableName = _.toCamelCase

  override def entityName = _.toCamelCase

  override def packageCode(profile: String, pkg: String, container: String, parentType: Option[String]): String = {
    s"""
package $pkg

$code
      """.trim
  }


  final val hasParents: String => Boolean = args => args.contains("created_time") && args.contains("updated_time")

  override def code = {
    s"""

import java.sql.Timestamp

import org.joda.time.DateTime
import slick.driver.MySQLDriver.api._

abstract class BaseTable[T](_tag: Tag, _schemaName: Option[String], _tableName: String) extends Table[T](_tag, _schemaName, _tableName) {

  def this(tag: Tag, tableName: String) = this(tag, None, tableName)

  implicit val dataTimeColumnType = MappedColumnType.base[DateTime, Timestamp](
    dt => new Timestamp(new DateTime().getMillis), ts => new DateTime(ts.getTime)
  )

  val id = column[String]("id", O.PrimaryKey, O.Length(32, varying = true))

  val created_time = column[Long]("created_time")

  val updated_time = column[Long]("updated_time")
}


trait BaseEntity {
  def id: Option[String]

  def created_time: Long

  def updated_time: Long
}


object Models{

${tables.map(_.code.mkString("\n")).mkString("\n\n")}

}
      """.trim
  }


  override def Table = new Table(_) {
    val E = entityName(model.name.table)
    val T = tableName(model.name.table)
    val dataColumns = columns
      // not include auto inc columns
      .filterNot(_.autoInc)
      // not include foreign keys
      .filterNot(c => model.foreignKeys.map(_.referencingColumns.head.name) contains c.model.name)

    val mappingColumns: Seq[Column] => String = columns => columns.map {
      case c if c.rawName == "id" => s"${c.rawName}: Option[${c.exposedType}] = None"
      //      case c if c.rawName == "type" => s"${c.name}: ${c.exposedType}"
      case c => c.default.map(v => s"${c.name}: ${c.exposedType} = $v").getOrElse(s"${c.name}: ${c.exposedType}")
    }.sortBy(_.split(":").head.length).mkString(", ")

    final val ||-> : Column => Boolean = c => c.rawName == "id" || c.rawName == "created_time" || c.rawName == "updated_time"

    override def PlainSqlMapper = new PlainSqlMapper {
      override def enabled = false
    }

    override def autoIncLastAsOption = true

    override def EntityType = new EntityType {

      override def doc = ""

      override def code = {
        val args = mappingColumns(columns)
        if (classEnabled) {
          if (hasParents(args)) {
            val prns = (Seq("BaseEntity").take(1).map(" extends " + _) ++ parents.drop(1).map(" with " + _)).mkString("")
            s"""case class $name($args)$prns"""
          }
          else s"""case class $name($args)"""
        } else {
          s"""
type $name = $types
/** Constructor for $name providing default values if available in the database schema. */
def $name($args): $name = {${compoundValue(columns.map(_.name.toString))}}
          """.trim
        }
      } + "\n"
    }

    override def TableClass = new TableClass {

      final val column2Str = mappingColumns(columns)

      final val sortedColumns = columns.sortBy(_.rawName.length)

      override def rawName = T + "Table"

      override def doc = ""

      override def optionFactory = {
        val accessors = sortedColumns.zipWithIndex.map { case (c, i) =>
          val accessor = if (sortedColumns.size > 1) tuple(i) else "r"
          if (c.fakeNullable || c.model.nullable || c.rawName == "id") accessor else s"$accessor.get"
        }
        val fac = s"$factory(${compoundValue(accessors)})"
        val discriminator = sortedColumns.zipWithIndex.collect { case (c, i) if !c.model.nullable => if (sortedColumns.size > 1) tuple(i) else "r" }.headOption
        val expr = discriminator.map(d => s"$d.map(_=> $fac)").getOrElse(s"None")
        if (sortedColumns.size > 1)
          s"{r=>import r._; $expr}"
        else
          s"r => $expr"
      }


      def OptionDef = new Def {
        def doc = "" //"Maps whole row to an option. Useful for outer joins."

        override def enabled = optionEnabled

        def code = {
          val struct = compoundValue(sortedColumns.map(c => if (c.model.nullable) s"${c.name}" else s"${c.name}.?"))
          val rhs = if (mappingEnabled) s"""$struct.shaped.<>($optionFactory, (_:Any) =>  throw new Exception("Inserting into ? projection not supported."))""" else struct
          s"def ? = $rhs"
        }

        def rawName = ???
      }

      def StarDef = new Def {
        def doc = ""

        def code = {
          val struct = compoundValue(sortedColumns.map(c => if (c.fakeNullable || c.rawName == "id") s"${c.name}.?" else s"${c.name}"))
          val rhs = if (mappingEnabled) s"$struct <> ($factory, $extractor)" else struct
          s"def * = $rhs"
        }

        def rawName = ???
      }

      override def definitions = {
        val columns = if (hasParents(column2Str)) sortedColumns.filterNot(||->) else sortedColumns

        Seq[Seq[Def]](
          Seq(StarDef, OptionDef), columns, primaryKey.toSeq, foreignKeys, indices
        )
      }

      override def body = definitions.map(_.flatMap(_.getEnabled).map(_.docWithCode)).filter(_.nonEmpty)

      override def code = {
        val args = model.name.schema.map(n => s"""Some("$n")""") ++ Seq("\"" + model.name.table + "\"")
        //        if (hasParents(column2Str)) {
        //          s"""
        //abstract class $name(_tableTag: Tag) extends BaseTable[$elementType](_tableTag, ${args.mkString(", ")}) {
        //  ${indent(body.map(_.mkString("\n")).mkString("\n\n"))}
        //}
        //        """.trim()
        //        } else {}
        s"""
class $name(_tableTag: Tag) extends ${if (hasParents(column2Str)) "BaseTable" else "Table"}[$elementType](_tableTag, ${args.mkString(", ")}) {
  ${indent(body.map(_.mkString("\n")).mkString("\n\n"))}
}
        """.trim()
      }
    }


    override def Column = new Column(_) {
      override def doc = ""

      override def rawName = model.name
    }

    override def TableValue = new TableValue {
      override def enabled = false

      override def rawName = super.rawName.head.toString.toLowerCase + super.rawName.tail

      override def code = s"lazy val $name =  TableQuery[$T]" //s"object $name extends TableQuery(tag => new $T(tag))"

    }


    override def code = super.code

  }

}

